import libtcodpy as libtcod
import textwrap
import os
import math
import shelve
from ConfigParser import SafeConfigParser
import sys

#actual size of the window
SCREEN_WIDTH = 120
SCREEN_HEIGHT = 50
 
#size of the map portion shown on-screen
CAMERA_WIDTH = 80
CAMERA_HEIGHT = 43
 
#size of the map
MAP_WIDTH = 120
MAP_HEIGHT = 70
 
#sizes and coordinates relevant for the GUI
BAR_WIDTH = 20
PANEL_HEIGHT = 7
PANEL_Y = SCREEN_HEIGHT - PANEL_HEIGHT
MSG_X = BAR_WIDTH + 2
MSG_WIDTH = SCREEN_WIDTH - BAR_WIDTH - 2
MSG_HEIGHT = 6 - 1
INVENTORY_WIDTH = 50
MAGIC_BOOK_WIDTH = 50
 
#parameters for dungeon generator
ROOM_MAX_SIZE = 10
ROOM_MIN_SIZE = 6
MAX_ROOMS = 50
MAX_ROOM_MONSTERS = 3
MAX_ROOM_ITEMS = 2
 
#spell values
HEAL_AMOUNT = 4
LIGHTNING_RANGE = 5
CONFUSE_RANGE = 8
CONFUSE_NUM_TURNS = 10
FIREBALL_RADIUS = 3

#modifiers
LEVEL_MODIFIER = 1
DUNGEON_LEVEL = 1
LEVEL_CAP = 3

#slots
TORSO = 0
LEGS = 1
FEET = 2
HANDS = 3
HEAD = 4
RIGHT_HAND = 5
LEFT_HAND = 6

#item type
CAST_HEAL = 1
CAST_FIREBALL = 2
CAST_LIGHTNING = 3
CAST_CONFUSE = 4
WEAR = 5
POTION = 6
SCROLL = 7

FOV_ALGO = 0  #default FOV algorithm
FOV_LIGHT_WALLS = True  #light walls or not
TORCH_RADIUS = 10
 
LIMIT_FPS = 20  #20 frames-per-second maximum
 
#floor and wall colors
color_dark_wall = libtcod.darker_grey
color_light_wall = libtcod.white
color_dark_ground = libtcod.dark_grey
color_light_ground = libtcod.white

#font
font = os.path.join('data', 'fonts', 'terminal.png')
libtcod.console_set_custom_font(font, libtcod.FONT_LAYOUT_ASCII_INROW)
libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'Dangerous Dungeons of Seveholm', False)
libtcod.sys_set_fps(LIMIT_FPS)
con = libtcod.console_new(MAP_WIDTH, MAP_HEIGHT)
panel = libtcod.console_new(SCREEN_WIDTH, PANEL_HEIGHT)

class Tile:
    #a tile of the map and its properties
    def __init__(self, blocked, block_sight = None):
        self.blocked = blocked
 
        #all tiles start unexplored
        self.explored = False
 
        #by default, if a tile is blocked, it also blocks sight
        if block_sight is None: block_sight = blocked
        self.block_sight = block_sight
 
class Rect:
    #a rectangle on the map. used to characterize a room.
    def __init__(self, x, y, w, h):
        self.x1 = x
        self.y1 = y
        self.x2 = x + w
        self.y2 = y + h
 
    def center(self):
        center_x = (self.x1 + self.x2) / 2
        center_y = (self.y1 + self.y2) / 2
        return (center_x, center_y)
 
    def intersect(self, other):
        #returns true if this rectangle intersects with another one
        return (self.x1 <= other.x2 and self.x2 >= other.x1 and
                self.y1 <= other.y2 and self.y2 >= other.y1)
 
class Object:
    #this is a generic object: the player, a monster, an item, the stairs...
    #it's always represented by a character on screen.
    def __init__(self, x, y, char, name, color, blocks=False, fighter=None, ai=None, item=None):
        self.x = x
        self.y = y
        self.char = char
        self.char_backup = char
        self.name = name
        self.color = color
        self.blocks = blocks
        self.fighter = fighter
        
        if self.fighter:  #let the fighter component know who owns it
            self.fighter.owner = self
 
        self.ai = ai
        if self.ai:  #let the AI component know who owns it
            self.ai.owner = self
 
        self.item = item
        if self.item:  #let the Item component know who owns it
            self.item.owner = self
 
    def move(self, dx, dy):
        #move by the given amount, if the destination is not blocked
        if not is_blocked(self.x + dx, self.y + dy):
            self.x += dx
            self.y += dy
 
    def move_towards(self, target_x, target_y):
        #vector from this object to the target, and distance
        dx = target_x - self.x
        dy = target_y - self.y
        distance = math.sqrt(dx ** 2 + dy ** 2)
 
        #normalize it to length 1 (preserving direction), then round it and
        #convert to integer so the movement is restricted to the map grid
        dx = int(round(dx / distance))
        dy = int(round(dy / distance))
        self.move(dx, dy)
 
    def distance_to(self, other):
        #return the distance to another object
        dx = other.x - self.x
        dy = other.y - self.y
        return math.sqrt(dx ** 2 + dy ** 2)
 
    def distance(self, x, y):
        #return the distance to some coordinates
        return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2)
 
    def send_to_back(self):
        #make this object be drawn first, so all others appear above it if they're in the same tile.
        global objects
        objects.remove(self)
        objects.insert(0, self)
 
    def draw(self):
        #only show if it's visible to the player
        if libtcod.map_is_in_fov(fov_map, self.x, self.y):
            (x, y) = to_camera_coordinates(self.x, self.y)
 
            if x is not None:
                #set the color and then draw the character that represents this object at its position
                #libtcod.console_set_default_nd(con, self.color)

                libtcod.console_set_char_foreground(con, x, y, self.color)
                libtcod.console_set_char(con, x, y, self.char)

    def clear(self):
        #erase the character that represents this object    
        (x, y) = to_camera_coordinates(self.x, self.y)
        if x is not None:
            if libtcod.map_is_in_fov(fov_map, self.x, self.y):
                libtcod.console_put_char_ex(con, x, y, chr(250), libtcod.white, libtcod.black)

class Fighter:
    #combat-related properties and methods (monster, player, NPC).
    def __init__(self, str, dex, intell, death_function=None):
        self.max_hp = str * 5
        self.hp = str * 5
        self.max_mana = intell * 4
        self.mana = intell * 3
        self.str = str
        self.dex = dex
        self.defense = int((str * dex) / 100)
        self.attack_rate = int((str * dex) / 7)
        self.int = intell
        self.death_function = death_function
        self.exp = 0
        self.exp_max = 100
        self.level = 1

        self.armor = None
 
    def attack(self, target):
        #a simple formula for attack damage
        seed = libtcod.random_get_int(0, -1, 1)
        damage = (self.attack_rate - (target.fighter.defense / 2)) + seed
 
        if damage > 0:
            #make the target take some damage
            message(self.owner.name.capitalize() + ' attacks ' + target.name + ' for ' + str(damage) + ' hit points.')
            target.fighter.take_damage(damage)
        else:
            message(self.owner.name.capitalize() + ' attacks ' + target.name + ' but it has no effect!')
 
    def take_damage(self, damage):
        #apply damage if possible
        if damage > 0:
            self.hp -= damage
 
            #check for death. if there's a death function, call it
            if self.hp <= 0:
                function = self.death_function
                if function is not None:
                    function(self.owner)

    def level_up(self):
        #message
        message('You gained ' + str(player.fighter.level) + ' level!', libtcod.green)
        self.level += 1
        self.exp = 0
        self.exp_max *= 2
        self.str = int(self.str * 1.55)
        self.int = int(self.int * 1.55)
        self.dex = int(self.dex * 1.55)

        self.recount()

        self.defense = int((self.str * self.dex) / 100)
        self.attack_rate = int((self.str * self.dex) / 7)

        self.hp = self.max_hp
        self.mana = self.max_mana
 
    def recount(self):
        self.max_hp = self.str * 5
        self.max_mana = self.int * 4

    def heal(self, amount):
        #heal by the given amount, without going over the maximum
        self.hp += amount
        if self.hp > self.max_hp:
            self.hp = self.max_hp

class BasicMonster:
    #AI for a basic monster.
    def take_turn(self):
        #a basic monster takes its turn. if you can see it, it can see you
        monster = self.owner
        if libtcod.map_is_in_fov(fov_map, monster.x, monster.y):
 
            #move towards player if far away
            if monster.distance_to(player) >= 2:
                monster.move_towards(player.x, player.y)
 
            #close enough, attack! (if the player is still alive.)
            elif player.fighter.hp > 0:
                monster.fighter.attack(player)

class ConfusedMonster:
    #AI for a temporarily confused monster (reverts to previous AI after a while).
    def __init__(self, old_ai, num_turns=CONFUSE_NUM_TURNS):
        self.old_ai = old_ai
        self.num_turns = num_turns
 
    def take_turn(self):
        if self.num_turns > 0:  #still confused...
            #move in a random direction, and decrease the number of turns confused
            self.owner.move(libtcod.random_get_int(0, -1, 1), libtcod.random_get_int(0, -1, 1))
            self.num_turns -= 1
 
        else:  #restore the previous AI (this one will be deleted because it's not referenced anymore)
            self.owner.ai = self.old_ai
            message('The ' + self.owner.name + ' is no longer confused!', libtcod.red)

class Item:
    #an item that can be picked up and used.
    def __init__(self, use_function=None, type=None, bonus=None):
        self.use_function = use_function
        self.type = type
        self.bonus = bonus
 
    def pick_up(self):
        #add to the player's inventory and remove from the map
        if len(inventory) >= 26:
            message('Your inventory is full, cannot pick up ' + self.owner.name + '.', libtcod.red)
        else:
            inventory.append(self.owner)
            objects.remove(self.owner)
            message('You picked up a ' + self.owner.name + '!', libtcod.green)
 
    def drop(self):
        #add to the map and remove from the player's inventory. also, place it at the player's coordinates
        objects.append(self.owner)
        inventory.remove(self.owner)
        self.owner.x = player.x
        self.owner.y = player.y
        message('You dropped a ' + self.owner.name + '.', libtcod.yellow)
 
    def use(self):
        #just call the "use_function" if it is defined
        if self.use_function is None:
            message('The ' + self.owner.name + ' cannot be used.')
        else:
            if self.use_function(owner=self) != 'cancelled':
                inventory.remove(self.owner)  #destroy after use, unless it was cancelled for some reason

class Spell:
    def __init__(self, mana_cost, use_function=None):
        self.mana_cost = mana_cost
        self.use_function = use_function

    def learn(self):
        #add to the player's magic book
        if len(magic_book) >= player.fighter.int:
            message('You cannot learn anymore!' + self.owner.name + '.', libtcod.red)
        else:
            magic_book.append(self.owner)
            message('You learned a ' + self.owner.name + '!', libtcod.green)
 
    def use(self):
        #just call the "use_function" if it is defined
        if self.use_function is None:
            message('The ' + self.owner.name + ' cannot be used.')
        else:
            if self.use_function() != 'cancelled':
                player.fighter.mana -= self.mana_cost

def is_blocked(x, y):
    #first test the map tile
    if map[x][y].blocked:
        return True
 
    #now check for any blocking objects
    for object in objects:
        if object.blocks and object.x == x and object.y == y:
            return True
 
    return False

def create_room(room):
    global map
    #go through the tiles in the rectangle and make them passable
    for x in range(room.x1 + 1, room.x2):
        for y in range(room.y1 + 1, room.y2):
            map[x][y].blocked = False
            map[x][y].block_sight = False
 
def create_h_tunnel(x1, x2, y):
    global map
    #horizontal tunnel. min() and max() are used in case x1>x2
    for x in range(min(x1, x2), max(x1, x2) + 1):
        map[x][y].blocked = False
        map[x][y].block_sight = False
 
def create_v_tunnel(y1, y2, x):
    global map
    #vertical tunnel
    for y in range(min(y1, y2), max(y1, y2) + 1):
        map[x][y].blocked = False
        map[x][y].block_sight = False

def make_hero():
    global player

    name = '_'
    race = '_'
    timer = 0
    command = ""

    str = 0
    dex = 0
    int = 0

    x = 1
    y = 3

    libtcod.console_set_default_background(panel, libtcod.black)
    libtcod.console_clear(panel)

    while not libtcod.console_is_window_closed():

        libtcod.console_print(panel, 1, 0,
                               "When you awoke in a cave, you see the old man. Seeing that you woke up, he said: ")
        libtcod.console_print(panel, 1, 1,
                               "\"Greetings, stranger. What is your name?\"")
        key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)

        timer += 1
        if timer % (LIMIT_FPS // 4) == 0:
            if timer % (LIMIT_FPS // 2) == 0:
                timer = 0
                libtcod.console_set_char(panel, x,  y, "_")
            else:
                libtcod.console_set_char(panel, x,  y, " ")

        if key.vk == libtcod.KEY_BACKSPACE and x > 0:
            libtcod.console_set_char(panel, x, y, " ")
            command = command[:-1]
            x -= 1
            
        elif key.vk == libtcod.KEY_ENTER or key.vk == libtcod.KEY_KPENTER:
            break
        
        elif key.c > 0:
            letter = chr(key.c)
            libtcod.console_set_char(panel, x, y, letter)
            command += letter
            x += 1
        
        libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
        libtcod.console_flush()

    name = command

    libtcod.console_clear(panel)

    libtcod.console_print(panel, 1, 0,
                               "I found you lying unconscious in a littered with mines. You're just like me - in a trap.")
    libtcod.console_print(panel, 1, 1,
                               "I'm too old to fight, all hope for you. What skills do you possess?")
    libtcod.console_print(panel, x, 3,
                               "(a) - warrior")
    libtcod.console_print(panel, x + 15, 3,
                               "(b) - rogue")
    libtcod.console_print(panel, x, 5,
                               "(c) - mage")
    libtcod.console_print(panel, x + 15, 5,
                               "(d) - monk")
    libtcod.console_print(panel, x + 30, 5,
                               "(e) - knight")

    while not libtcod.console_is_window_closed():
        
        key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)

        if key.c == ord('a'):
            str = 11 + 1
            dex = 4 + 1
            int = 4 + 1
            break

        elif key.c == ord('b'):
            dex = 8 + 1
            str = 5 + 1
            int = 4 + 1
            break

        elif key.c == ord('c'):
            int = 8 + 1
            dex = 4 + 1
            str = 4 + 1
            break

        elif key.c == ord('d'):
            int = 6 + 1
            dex = 6 + 1
            str = 6 + 1
            break

        elif key.c == ord('e'):
            int = 7 + 1
            dex = 4 + 1
            str = 7 + 1
            break

        libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
        libtcod.console_flush()

    libtcod.console_clear(panel)

    libtcod.console_print(panel, 1, 0,
                               "Excellent. As I can see, you are ready for battle. Where were you born?")
    libtcod.console_print(panel, x, 2,
                               "(a) - from Daggestone (Orc)")
    libtcod.console_print(panel, x, 4,
                               "(b) - from Coverhole (Goblin)")
    libtcod.console_print(panel, 40, 2,
                               "(c) - from Blackuard (Human)")
    libtcod.console_print(panel, 40, 4,
                               "(d) - from Fallenwell (Gnome)")
    libtcod.console_print(panel, 80, 4,
                               "(f) - from Wonderfall (Elf)")
    libtcod.console_print(panel, 80, 2,
                               "(e) - from Coldwinter Dale (Dwarf)")

    while not libtcod.console_is_window_closed():
        
        key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)

        if key.c == ord('a'):
            race = libtcod.dark_green
            break

        elif key.c == ord('b'):
            race = libtcod.grey
            break

        elif key.c == ord('c'):
            race = libtcod.white
            break

        elif key.c == ord('d'):
            race = libtcod.lighter_grey
            break
        
        elif key.c == ord('e'):
            race = libtcod.sepia
            break

        elif key.c == ord('f'):
            race = libtcod.yellow
            break

        libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
        libtcod.console_flush()

    libtcod.console_clear(panel)
    libtcod.console_print(panel, 1, 0,
                               "I see you've regained consciousness, and I think that we need to start the path. Take a candle and...")
    libtcod.console_print(panel, 1, 2,
                               "After this poor old man falls to the floor, bleeding. He took an arrow in the knee. You take the candle...")
    
    while not libtcod.console_is_window_closed():
        
        key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)

        if key.c:
            break

        libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
        libtcod.console_flush()

    stats = Fighter(str, dex, int, death_function=player_death)
    player = Object(0, 0, '@', name, race, blocks=True, fighter=stats)

def make_map():
    global map, objects
 
    #the list of objects with just the player
    objects = [player]
 
    #fill map with "blocked" tiles
    map = [[ Tile(True)
        for y in range(MAP_HEIGHT) ]
            for x in range(MAP_WIDTH) ]
 
    rooms = []
    num_rooms = 0
 
    for r in range(MAX_ROOMS):
        #random width and height
        w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
        h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)
        #random position without going out of the boundaries of the map
        x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1)
        y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)
 
        #"Rect" class makes rectangles easier to work with
        new_room = Rect(x, y, w, h)
 
        #run through the other rooms and see if they intersect with this one
        failed = False
        for other_room in rooms:
            if new_room.intersect(other_room):
                failed = True
                break
 
        if not failed:
            #this means there are no intersections, so this room is valid
 
            #"paint" it to the map's tiles
            create_room(new_room)
 
            #add some contents to this room, such as monsters
            place_objects(new_room)
 
            #center coordinates of new room, will be useful later
            (new_x, new_y) = new_room.center()

            if num_rooms == 0:
                #this is the first room, where the player starts at
                player.x = new_x
                player.y = new_y
                
            else:
                #all rooms after the first:
                #connect it to the previous room with a tunnel
 
                #center coordinates of previous room
                (prev_x, prev_y) = rooms[num_rooms - 1].center()
 
                #draw a coin (random number that is either 0 or 1)
                if libtcod.random_get_int(0, 0, 1) == 1:
                    #first move horizontally, then vertically
                    create_h_tunnel(prev_x, new_x, prev_y)
                    create_v_tunnel(prev_y, new_y, new_x)
                else:
                    #first move vertically, then horizontally
                    create_v_tunnel(prev_y, new_y, prev_x)
                    create_h_tunnel(prev_x, new_x, new_y)
 
            #finally, append the new room to the list
            rooms.append(new_room)
            num_rooms += 1

def place_objects(room):
    global stairs

    #choose random number of monsters
    num_monsters = libtcod.random_get_int(0, 0, MAX_ROOM_MONSTERS)
 
    for i in range(num_monsters):
        #choose random spot for this monster
        x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
        y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
 
        #only place it if the tile is not blocked
        if not is_blocked(x, y):

            monster = get_random_monster(x, y, LEVEL_MODIFIER)
 
            objects.append(monster)

    #choose random number of items
    num_items = libtcod.random_get_int(0, 0, MAX_ROOM_ITEMS)
 
    for i in range(num_items):
        #choose random spot for this item
        x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
        y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
 
        #only place it if the tile is not blocked
        if not is_blocked(x, y):
            item = get_random_item(x, y)
            objects.append(item)
            item.send_to_back()  #items appear below other objects

    create = libtcod.random_get_int(0, 1, 2)

    if create == 1:
        #choose random number of items
        num_stairs = libtcod.random_get_int(0, 1, 2)
     
        for i in range(num_stairs):
            #choose random spot for this item
            x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
            y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
     
            #only place it if the tile is not blocked
            if not is_blocked(x, y):
                dice = libtcod.random_get_int(0, 0, 100)
                if dice < 50:
                    stair = Object(x, y, '>', 'down_stairway', libtcod.white)
                    stairs.append(stair)
                elif dice > 50:
                    stair = Object(x, y, '<', 'up_stairway', libtcod.white)
                    stairs.append(stair)

def get_names_under_mouse():
    #return a string with the names of all objects under the mouse
    mouse = libtcod.mouse_get_status()
    (x, y) = (mouse.cx, mouse.cy)
    (x, y) = (camera_x + x, camera_y + y)  #from screen to map coordinates
 
    #create a list with the names of all objects at the mouse's coordinates and in FOV
    names = [obj.name for obj in objects
        if obj.x == x and obj.y == y and libtcod.map_is_in_fov(fov_map, obj.x, obj.y)]
 
    names = ', '.join(names)  #join the names, separated by commas
    return names.capitalize()

def move_camera(target_x, target_y):
    global camera_x, camera_y, fov_recompute
 
    #new camera coordinates (top-left corner of the screen relative to the map)
    x = target_x - CAMERA_WIDTH / 2  #coordinates so that the target is at the center of the screen
    y = target_y - CAMERA_HEIGHT / 2
 
    #make sure the camera doesn't see outside the map
    if x < 0: x = 0
    if y < 0: y = 0
    if x > MAP_WIDTH - CAMERA_WIDTH - 1: x = MAP_WIDTH - CAMERA_WIDTH - 1
    if y > MAP_HEIGHT - CAMERA_HEIGHT - 1: y = MAP_HEIGHT - CAMERA_HEIGHT - 1
 
    if x != camera_x or y != camera_y: fov_recompute = True
 
    (camera_x, camera_y) = (x, y)
 
def to_camera_coordinates(x, y):
    #convert coordinates on the map to coordinates on the screen
    (x, y) = (x - camera_x, y - camera_y)
 
    if (x < 0 or y < 0 or x >= CAMERA_WIDTH or y >= CAMERA_HEIGHT):
        return (None, None)  #if it's outside the view, return nothing
 
    return (x, y)

def render_all():
    global fov_map, color_dark_wall, color_light_wall
    global color_dark_ground, color_light_ground
    global fov_recompute
 
    move_camera(player.x, player.y)
 
    if fov_recompute:
        #recompute FOV if needed (the player moved or something)
        fov_recompute = False
        libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
        libtcod.console_clear(con)
 
        #go through all tiles, and set their background color according to the FOV
        for y in range(CAMERA_HEIGHT):
            for x in range(CAMERA_WIDTH):
                (map_x, map_y) = (camera_x + x, camera_y + y)
                visible = libtcod.map_is_in_fov(fov_map, map_x, map_y)
 
                wall = map[map_x][map_y].block_sight
                if not visible:
                    #if it's not visible right now, the player can only see it if it's explored
                    if map[map_x][map_y].explored:
                        if wall:
                            libtcod.console_put_char_ex(con, x, y, 177, color_dark_wall, libtcod.BKGND_SET)
                        else:
                            libtcod.console_put_char_ex(con, x, y, 250, color_dark_ground, libtcod.BKGND_SET)
                else:
                    #it's visible
                    if wall:
                        libtcod.console_put_char_ex(con, x, y, 177, color_light_wall, libtcod.BKGND_SET)
                    else:
                        libtcod.console_put_char_ex(con, x, y, 250, color_light_ground, libtcod.BKGND_SET)
                    #since it's visible, explore it
                    map[map_x][map_y].explored = True
 
    #draw all objects in the list, except the player. we want it to
    #always appear over all other objects! so it's drawn later.
    for stair in stairs:
        stair.draw()
    for object in objects:
        if object != player:
            object.draw()
    
    player.draw()
 
    #blit the contents of "con" to the root console
    libtcod.console_blit(con, 0, 0, MAP_WIDTH, MAP_HEIGHT, 0, 0, 0)
 
    #prepare to render the GUI panel
    libtcod.console_set_default_background(panel, libtcod.black)
    libtcod.console_clear(panel)

    libtcod.console_print(panel, 1, 1,
                               "Name : " + player.name)
    libtcod.console_print(panel, 39, 1,
                               "Level : " + str(player.fighter.level))
    libtcod.console_print(panel, 20, 1,
                               "%c%c%c%cSTRENGHT%c : "%(libtcod.COLCTRL_FORE_RGB,255,1,1,libtcod.COLCTRL_STOP) + str(player.fighter.str))
    libtcod.console_print(panel, 20, 3,
                               "%c%c%c%cDEXTERITY%c : "%(libtcod.COLCTRL_FORE_RGB,1,255,1,libtcod.COLCTRL_STOP) + str(player.fighter.dex))    
    libtcod.console_print(panel, 20, 5,
                               "%c%c%c%cINTELLIGENCE%c : "%(libtcod.COLCTRL_FORE_RGB,1,1,255,libtcod.COLCTRL_STOP) + str(player.fighter.int))            
    libtcod.console_print(panel, 1, 3,
                               "%c%c%c%cEXP%c : "%(libtcod.COLCTRL_FORE_RGB, 120, 20, 20, libtcod.COLCTRL_STOP) + str(player.fighter.exp))             
    libtcod.console_print(panel, 1, 5,
                               "%c%c%c%cNEXT LEVEL%c : "%(libtcod.COLCTRL_FORE_RGB, 170, 20, 20, libtcod.COLCTRL_STOP) + str(player.fighter.exp_max))  
    libtcod.console_print(panel, 39, 3,
                               "%c%c%c%cHit Points%c : "%(libtcod.COLCTRL_FORE_RGB,255,1,1,libtcod.COLCTRL_STOP) + str(player.fighter.hp) + "/" + str(player.fighter.max_hp))
    libtcod.console_print(panel, 39, 5,
                               "%c%c%c%cMana Points%c : "%(libtcod.COLCTRL_FORE_RGB,1,1,255,libtcod.COLCTRL_STOP) + str(player.fighter.mana) + "/" + str(player.fighter.max_mana))

    y = 1
    for (line, color) in game_msgs:
        libtcod.console_set_default_foreground(panel, color)
        libtcod.console_print(panel, 60, y, line)
        y += 1

    libtcod.console_set_default_foreground(panel, libtcod.white)

    libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)

def message(new_msg, color = libtcod.white):
    #split the message if necessary, among multiple lines
    new_msg_lines = textwrap.wrap(new_msg, MSG_WIDTH)
 
    for line in new_msg_lines:
        #if the buffer is full, remove the first line to make room for the new one
        if len(game_msgs) == MSG_HEIGHT:
            del game_msgs[0]
 
        #add the new line as a tuple, with the text and the color
        game_msgs.append( (line, color) )

def player_move_or_attack(dx, dy):
    global fov_recompute
 
    #the coordinates the player is moving to/attacking
    x = player.x + dx
    y = player.y + dy
 
    #try to find an attackable object there
    target = None
    for object in objects:
        if object.fighter and object.x == x and object.y == y:
            target = object
            break
 
    #attack if target found, move otherwise
    if target is not None:
        player.fighter.attack(target)
    else:
        player.move(dx, dy)
        fov_recompute = True

def menu(header, options, width):
    if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.')
 
    #calculate total height for the header (after auto-wrap) and one line per option
    header_height = libtcod.console_get_height_rect(con, 0, 0, width, SCREEN_HEIGHT, header)
    if header == '':
        header_height = 0
    height = len(options) + header_height
 
    #create an off-screen console that represents the menu's window
    window = libtcod.console_new(width, height)
 
    #print the header, with auto-wrap
    libtcod.console_set_default_foreground(window, libtcod.white)
    libtcod.console_print_rect(window, 0, 0, width, height, header)
 
    #print all the options
    y = header_height
    letter_index = ord('a')
    for option_text in options:
        text = '(' + chr(letter_index) + ') ' + option_text
        libtcod.console_print(window, 0, y, text)
        y += 1
        letter_index += 1
 
    #blit the contents of "window" to the root console
    x = SCREEN_WIDTH/2 - width/2
    y = SCREEN_HEIGHT/2 - height/2
    libtcod.console_blit(window, 0, 0, width, height, 0, x, y, 1.0, 0.7)
 
    #present the root console to the player and wait for a key-press
    libtcod.console_flush()

    key = libtcod.console_wait_for_keypress(True)

    #convert the ASCII code to an index; if it corresponds to an option, return it
    index = key.c - ord('a')
    if index >= 0 and index < len(options): return index
    return None
 
def inventory_menu(header):
    #show a menu with each item of the inventory as an option
    if len(inventory) == 0:
        options = ['Inventory is empty.']
    else:
        options = [item.name for item in inventory]
 
    index = menu(header, options, INVENTORY_WIDTH)
 
    #if an item was chosen, return it
    if index is None or len(inventory) == 0: return None
    return inventory[index].item

def magic_menu(header):
    #show a menu with each item of the inventory as an option
    if len(magic_book) == 0:
        options = ['Magic book is empty.']
    else:
        options = [item.name for item in magic_book]
 
    index = menu(header, options, MAGIC_BOOK_WIDTH)
 
    #if an spell was chosen, return it
    if index is None or len(magic_book) == 0: return None
    return magic_book[index].item

def wear(owner):
    if armor[owner.type] != None:
        unbonus(owner.bonus)
        player.fighter.recount()
    armor[owner.type] = owner
    bonus(owner.bonus)
    player.fighter.recount()

def char_menu(header):
    torso = armor[0]
    legs = armor[1]
    feet = armor[2]
    hands = armor[3]
    head = armor[4]
    right_hand = armor[5]
    left_hand = armor[6]

    options = []

    if torso == None:
        torso = 'None'
        options.append("Torso: None")
    else:
        options.append("Torso: " + torso.owner.name)

    if legs == None:
        legs = 'None'
        options.append("Legs: None")
    else:
        options.append("Legs: " + legs.owner.name)

    if feet == None:
        feet = 'None'
        options.append("Feet: None")
    else:
        options.append("Feets: " + feets.owner.name)

    if hands == None:
        hands = 'None'
        options.append("Hands: None")
    else:
        options.append("Hands: " + hands.owner.name)

    if head == None:
        head = 'None'
        options.append("Head: None")
    else:
        options.append("Head: " + head.owner.name)

    if right_hand == None:
        right_hand = 'None'
        options.append("Right Hand: None")
    else:
        options.append("Right Hand: " + right_hand.owner.name)

    if left_hand == None:
        left_hand = 'None'
        options.append("Left Hand: None")
    else:
        options.append("Left Hand: " + left_hand.owner.name)

    index = menu(header, options, INVENTORY_WIDTH)

def msgbox(text, width=50):
    menu(text, [], width)  #use menu() as a sort of "message box"

def handle_keys():
    global stair
    global LEVEL_MODIFIER, DUNGEON_LEVEL

    key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)
 
    if key.vk == libtcod.KEY_ENTER and key.lalt:
        libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
 
    elif key.vk == libtcod.KEY_ESCAPE:
        return 'exit'

    elif key.vk == libtcod.KEY_F10:
        libtcod.sys_save_screenshot('screen.png')
 
    if game_state == 'playing':
        if key.vk == libtcod.KEY_UP:
            player_move_or_attack(0, -1)
 
        elif key.vk == libtcod.KEY_DOWN:
            player_move_or_attack(0, 1)
 
        elif key.vk == libtcod.KEY_LEFT:
            player_move_or_attack(-1, 0)
 
        elif key.vk == libtcod.KEY_RIGHT:
            player_move_or_attack(1, 0)

        elif key.vk == libtcod.KEY_F6:
            save_game()

        elif key.vk == libtcod.KEY_F7:
            load_game()

        elif key.vk == libtcod.KEY_ENTER:
            for stair in stairs:
                if player.x == stair.x:
                    if player.y == stair.y:
                        libtcod.console_clear(con)

                        make_map()
                        initialize_fov()     
                         
                        fov_recompute = True
                                           
                        if DUNGEON_LEVEL < LEVEL_CAP:
                            DUNGEON_LEVEL += 1
                        LEVEL_MODIFIER = int(DUNGEON_LEVEL * 1.55)
 

        else:
            key_char = chr(key.c)
 
            if key_char == 'g':
                for object in objects:
                    if object.x == player.x and object.y == player.y and object.item:
                        object.item.pick_up()
                        break
 
            if key_char == 'm':
                chosen_spell = magic_menu('Press the key next to an spell to cast it, or any other to cancel.\n')
                if chosen_spell is not None:
                    chosen_spell.use()

                    return 'cast'

            if key_char == 'c':
                chosen_spell = char_menu('Press any other to cancel.\n')

            if key_char == 'i':
                chosen_item = inventory_menu('Press the key next to an item to use it, or any other to cancel.\n')
                if chosen_item is not None:
                    chosen_item.use()

                    return 'item'

            if key_char == 'd':
                chosen_item = inventory_menu('Press the key next to an item to drop it, or any other to cancel.\n')
                if chosen_item is not None:
                    chosen_item.drop()
 
            return 'didnt-take-turn'

def player_death(player):
    #the game ended!
    global game_state
    message('', libtcod.red)
    game_state = 'dead'
 
    #for added effect, transform the player into a corpse!
    player.char = '%'
    player.color = libtcod.dark_red

    x = 1
    y = 3

    libtcod.console_set_default_background(panel, libtcod.black)
    libtcod.console_clear(panel)

    while not libtcod.console_is_window_closed():

        libtcod.console_print(panel, 20, 1,
                               "%c%c%c%cYou died!%c"%(libtcod.COLCTRL_FORE_RGB,255,1,1,libtcod.COLCTRL_STOP))

        key = libtcod.console_check_for_keypress(libtcod.KEY_PRESSED)
              
        if key.c > 0:
            sys.exit(0)
        
        libtcod.console_blit(panel, 0, 0, SCREEN_WIDTH, PANEL_HEIGHT, 0, 0, PANEL_Y)
        libtcod.console_flush()

    
def monster_death(monster):
    #transform it into a nasty corpse! it doesn't block, can't be
    #attacked and doesn't move

    player.fighter.exp += int((monster.fighter.str * monster.fighter.dex) * 1.25)

    if player.fighter.exp >= player.fighter.exp_max:
        player.fighter.level_up()

    message(monster.name.capitalize() + ' is dead!', libtcod.orange)
    monster.char = '%'
    monster.color = libtcod.dark_red
    monster.blocks = False
    monster.fighter = None
    monster.ai = None
    monster.name = 'remains of ' + monster.name
    monster.send_to_back()

def target_tile(max_range=None):
    #return the position of a tile left-clicked in player's FOV (optionally in a range), or (None,None) if right-clicked.
    while True:
        #render the screen. this erases the inventory and shows the names of objects under the mouse.
        render_all()
        libtcod.console_flush()
 
        key = libtcod.console_check_for_keypress()
        mouse = libtcod.mouse_get_status()  #get mouse position and click status
        (x, y) = (mouse.cx, mouse.cy)
        (x, y) = (camera_x + x, camera_y + y)  #from screen to map coordinates
 
        if mouse.rbutton_pressed or key.vk == libtcod.KEY_ESCAPE:
            return (None, None)  #cancel if the player right-clicked or pressed Escape
 
        #accept the target if the player clicked in FOV, and in case a range is specified, if it's in that range
        if (mouse.lbutton_pressed and libtcod.map_is_in_fov(fov_map, x, y) and
            (max_range is None or player.distance(x, y) <= max_range)):
            return (x, y)
 
def target_monster(max_range=None):
    #returns a clicked monster inside FOV up to a range, or None if right-clicked
    while True:
        (x, y) = target_tile(max_range)
        if x is None:  #player cancelled
            return None
 
        #return the first clicked monster, otherwise continue looping
        for obj in objects:
            if obj.x == x and obj.y == y and obj.fighter and obj != player:
                return obj
 
def closest_monster(max_range):
    #find closest enemy, up to a maximum range, and in the player's FOV
    closest_enemy = None
    closest_dist = max_range + 1  #start with (slightly more than) maximum range
 
    for object in objects:
        if object.fighter and not object == player and libtcod.map_is_in_fov(fov_map, object.x, object.y):
            #calculate distance between this object and the player
            dist = player.distance_to(object)
            if dist < closest_dist:  #it's closer, so remember it
                closest_enemy = object
                closest_dist = dist
    return closest_enemy

def regeneration():
    seed = libtcod.random_get_int(None, 1, 14)
    if player.fighter.hp <= 100:
        if seed == 5:
            player.fighter.hp += int(math.floor(player.fighter.str / 2.5))
            if player.fighter.hp > player.fighter.max_hp:
                player.fighter.hp = player.fighter.max_hp
    if player.fighter.mana <= 100:
        if seed == 7:
            player.fighter.mana += int(math.floor(player.fighter.int / 1.5))
            if player.fighter.mana > player.fighter.max_mana:
                player.fighter.mana = player.fighter.max_mana

def bonus(bonus):
    if bonus != None:
        player.fighter.str += bonus[0]
        player.fighter.dex += bonus[1]
        player.fighter.int += bonus[2]
        player.fighter.attack_rate += bonus[3]
        player.fighter.defense += bonus[4]
        if bonus[0] != 0:
            message('Your strenght is increased by ' + str(bonus[0]), libtcod.green)
        if bonus[1] != 0:
            message('Your dexterity is increased by ' + str(bonus[1]), libtcod.green)
        if bonus[2] != 0:
            message('Your intelligence is increased by ' + str(bonus[2]), libtcod.green)
        if bonus[3] != 0:
            message('Your attack rate is increased by ' + str(bonus[3]), libtcod.green)
        if bonus[4] != 0:
            message('Your defense is increased by ' + str(bonus[4]), libtcod.green)

def unbonus(bonus):
    if bonus != None:
        player.fighter.str -= bonus[0]
        player.fighter.dex -= bonus[1]
        player.fighter.int -= bonus[2]
        player.fighter.attack_rate -= bonus[3]
        player.fighter.defense -= bonus[4]
        if bonus[0] != 0:
            message('Your strenght is decreased by ' + str(bonus[0]), libtcod.green)
        if bonus[1] != 0:
            message('Your dexterity is decreased by ' + str(bonus[1]), libtcod.green)
        if bonus[2] != 0:
            message('Your intelligence is decreased by ' + str(bonus[2]), libtcod.green)
        if bonus[3] != 0:
            message('Your attack rate is decreased by ' + str(bonus[3]), libtcod.green)
        if bonus[4] != 0:
            message('Your defense is decreased by ' + str(bonus[4]), libtcod.green)
 
def cast_heal(owner=None):
    cost = int(player.fighter.int * 1.5)
    if player.fighter.mana >= cost:
        #heal the player
        if player.fighter.hp == player.fighter.max_hp:
            message('You are already at full health.', libtcod.red)
            return 'cancelled'
        player.fighter.mana -= cost
     
        message('Your wounds start to feel better!', libtcod.light_violet)
        player.fighter.heal(int(player.fighter.int * 3.5))
    else:
        message('Not enough mana.', libtcod.red)
        return 'cancelled'
 
def cast_lightning(owner=None):
    cost = int(player.fighter.int * 1.5)
    if player.fighter.mana >= cost:
        damage = int(player.fighter.int * 1.35)

        #find closest enemy (inside a maximum range) and damage it
        monster = closest_monster(LIGHTNING_RANGE)
        if monster is None:  #no enemy found within maximum range
            message('No enemy is close enough to strike.', libtcod.red)
            return 'cancelled'
        player.fighter.mana -= cost
     
        #zap it!
        message('A lighting bolt strikes the ' + monster.name + '! The damage is '
            + str(damage), libtcod.light_blue)
        monster.fighter.take_damage(damage)
    else:
        message('Not enough mana.', libtcod.red)
        return 'cancelled'
 
def cast_fireball(owner=None):
    cost = int(player.fighter.int * 1.5)
    if player.fighter.mana >= cost:
        #ask the player for a target tile to throw a fireball at
        message('Left-click a target tile for the fireball, or right-click to cancel.', libtcod.light_cyan)
        (x, y) = target_tile()
        if x is None: return 'cancelled'
        player.fighter.mana -= cost
        message('The fireball explodes, burning everything within ' + str(FIREBALL_RADIUS) + ' tiles!', libtcod.orange)

        damage = int(round(player.fighter.int * 1.54))
     
        for obj in objects:  #damage every fighter in range, including the player
            if obj.distance(x, y) <= FIREBALL_RADIUS and obj.fighter:
                message('The ' + obj.name + ' gets burned for ' + str(damage) + ' hit points.', libtcod.orange)
                obj.fighter.take_damage(damage)
    else:
        message('Not enough mana.', libtcod.red)
        return 'cancelled'
 
def cast_confuse(owner=None):
    cost = int(player.fighter.int * 1.5)
    if player.fighter.mana >= cost:
        #ask the player for a target to confuse
        message('Left-click an enemy to confuse it, or right-click to cancel.', libtcod.light_cyan)
        monster = target_monster(CONFUSE_RANGE)
        if monster is None: return 'cancelled'
        player.fighter.mana -= cost
        #replace the monster's AI with a "confused" one; after some turns it will restore the old AI
        old_ai = monster.ai
        monster.ai = ConfusedMonster(old_ai)
        monster.ai.owner = monster  #tell the new component who owns it
        message('The eyes of the ' + monster.name + ' look vacant, as he starts to stumble around!', libtcod.light_green)
    else:
        message('Not enough mana.', libtcod.red)
        return 'cancelled'

def get_random_monster(x, y, modifier):
    parser = SafeConfigParser()
    parser.read('monsterbase.dat')

    sections = parser.sections()

    array_of_sections = []

    for i in range(0, len(sections)):

        if int(parser.get(sections[i], 'level')) <= DUNGEON_LEVEL:

            array_of_sections.append(sections[i])

    random_int = libtcod.random_get_int(0, 0, len(array_of_sections) - 1)

    monster_section = array_of_sections[random_int]

    fighter_component = Fighter(int(parser.get(monster_section, 'str')) * modifier,
                                int(parser.get(monster_section, 'dex')) * modifier,
                                int(parser.get(monster_section, 'int')) * modifier, death_function=monster_death)

    ai_component = BasicMonster()

    monster = Object(x, y, parser.get(monster_section, 'char'), parser.get(monster_section, 'name'), 
                libtcod.Color(int(parser.get(monster_section, 'red')),
                                int(parser.get(monster_section, 'green')),
                                int(parser.get(monster_section, 'blue'))),
                blocks=True, fighter=fighter_component, ai=ai_component)

    return monster

def get_random_item(x, y):
    parser = SafeConfigParser()
    parser.read('itembase.dat')

    sections = parser.sections()

    array_of_sections = []

    for i in range(0, len(sections)):

        if int(parser.get(sections[i], 'level')) <= DUNGEON_LEVEL:

            array_of_sections.append(sections[i])

    random_int = libtcod.random_get_int(0, 0, len(array_of_sections) - 1)

    item_section = array_of_sections[random_int]

    #func, slot, bonus
    item_component = None
    #bonus array
    bonus = []
    #slot number
    slot = None

    #append bonus if item is wearable
    if parser.has_option(item_section, 'bonus_str') != False:
        bonus.append(int(parser.get(item_section, 'bonus_str'))) #strenght
        bonus.append(int(parser.get(item_section, 'bonus_dex'))) #dexterity
        bonus.append(int(parser.get(item_section, 'bonus_int'))) #intelligence
        bonus.append(int(parser.get(item_section, 'bonus_attack_rate'))) #attack rate
        bonus.append(int(parser.get(item_section, 'bonus_defense'))) #defense

    #append slot if item is wearable
    if parser.has_option(item_section, 'slot'):
        slot = int(parser.get(item_section, 'slot'))

    item_component = Item(use_function=eval(str(parser.get(item_section, 'func'))), 
                            type = slot, 
                            bonus=bonus)

    #item object
    item = Object(x, y, parser.get(item_section, 'char'), 
                    str(parser.get(item_section, 'name')), 
                    libtcod.Color(int(parser.get(item_section, 'red')),
                                int(parser.get(item_section, 'green')),
                                int(parser.get(item_section, 'blue'))), item=item_component)

    return item

def save_game():
    #open a new empty shelve (possibly overwriting an old one) to write the game data
    file = shelve.open('savegame', 'n')
    file['map'] = map
    file['objects'] = objects
    file['stairs'] = stairs
    file['player_index'] = objects.index(player)  #index of player in objects list
    file['inventory'] = inventory
    file['magic_book'] = magic_book
    file['game_msgs'] = game_msgs
    file['game_state'] = game_state
    file.close()
 
def load_game():
    #open the previously saved shelve and load the game data
    global map, objects, player, stairs, inventory, game_msgs, game_state
 
    file = shelve.open('savegame', 'r')
    map = file['map']
    objects = file['objects']
    stairs = file['stairs']
    player = objects[file['player_index']]  #get index of player in objects list and access it
    inventory = file['inventory']
    magic_book = file['magic_book']
    game_msgs = file['game_msgs']
    game_state = file['game_state']
    file.close()
 
    initialize_fov()
 
def new_game():
    global player, inventory, game_msgs, game_state, objects, magic_book, stairs, armor

    #name, class
    make_hero()

    #the list of objects with those two
    objects = [player]
    stairs = []

    #generate map (at this point it's not drawn to the screen)
    make_map()
    initialize_fov()
 
    game_state = 'playing'

    #create the list of game messages and their colors, starts empty
    game_msgs = []

    magic_book = []
    inventory = []
    armor = [None, None, None, None, None, None, None]

    if player.fighter.int > player.fighter.str and player.fighter.int > player.fighter.dex:
        fire = Spell(10, use_function=cast_fireball)
        spell = Object(None, None, None, 'Fireball', None, item=fire)
        fire.learn()

        heal = Spell(10, use_function=cast_heal)
        spell = Object(None, None, None, 'Heal', None, item=heal)
        heal.learn()
    
    if player.fighter.int == player.fighter.str:
        heal = Spell(10, use_function=cast_heal)
        spell = Object(None, None, None, 'Heal', None, item=heal)
        heal.learn()
 
def initialize_fov():
    global fov_recompute, fov_map
    fov_recompute = True
 
    #create the FOV map, according to the generated map
    fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)
    for y in range(MAP_HEIGHT):
        for x in range(MAP_WIDTH):
            libtcod.map_set_properties(fov_map, x, y, not map[x][y].blocked, not map[x][y].block_sight)
 
    libtcod.console_clear(con)  #unexplored areas start black (which is the default background color)
 
def play_game():
    global camera_x, camera_y
 
    player_action = None
    (camera_x, camera_y) = (0, 0)
 
    while not libtcod.console_is_window_closed():
        #render the screen
        render_all()
 
        libtcod.console_flush()

        for object in objects:
            object.clear()

        #handle keys and exit game if needed
        player_action = handle_keys()
        if player_action == 'exit':
            save_game()
            break

        #clear if player casted spell or used item
        if player_action == 'cast' or player_action == 'item':
            for object in objects:
                object.clear()
 
        #let monsters take their turn
        if game_state == 'playing' and player_action != 'didnt-take-turn':
            for object in objects:
                if object.ai:
                    object.ai.take_turn()

            regeneration()

new_game()
play_game()